Skip to main content

NodeJS-Express

  • The “front end” denotes the interface that a web user interacts with - what they see (and hear) when using the web.
  • The “back end”, meanwhile, denotes all that goes on “behind the scenes” on web servers to make the user experience possible.

I. NodeJS​

Node is an asynchronous event driven JavaScript runtime.

  • When JavaScript was first created, it was designed to run in the browser.
  • Node lets it run anywhere (your machine, a server)

Node adds capabilities JS didn't have in the browser:

  • Read/write local files
  • Create HTTP connections
  • Listen to network requests
Example
  • Browser JS:
// Change a button's text when clicked
document.querySelector("button").addEventListener("click", () => {
document.querySelector("h1").textContent = "Hello!";
});
  • Node.js — things only possible outside the browser
// Create a server that listens for requests
const http = require("http");
http.createServer((req, res) => {
res.end("Hello from the server!");
}).listen(3000);

Async / Event-driven​

Node doesn't wait for slow tasks (file I/O, DB queries) — it starts them and moves on

  • When a task finishes, it fires an event and runs the next function (callback)
  • Same idea as addEventListener in frontend JS, but for server-side events
  • Result: can handle many things at once without blocking
- Synchronous: Read file → wait → done → query DB → wait → done
- Node (async): Read file + query DB at the same time → handle whichever finishes first

III. Core Node Modules​

1. Running Scripts​

  • Node lets you run any JS file directly in the terminal (no browser needed)
  • node myfile.js → executes the file and prints output to your terminal
  • This is the most basic thing Node adds: run JS like a program, not a webpage

2. HTTP Module — creating a local server​

  • When you write frontend code, you're used to just opening an HTML file in the browser
  • In backend dev, you need an actual server running that "listens" for requests and sends back responses
    • http.createServer() sets up that server
    • .listen(3000) tells it which port to watch — port 3000 is just a common convention for local dev
  • While the script is running, anyone who visits http://localhost:3000 triggers your callback
    • "localhost" means your own machine — this server is only accessible to you locally, not the internet
    • In production, you'd deploy this to a real server with a real domain
Example:
  • req = the incoming request (who's asking, what page they want)
  • res = your response back to them
const http = require("http");
http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/html" }); // 200 = OK status code
res.end("Hello World!"); // what gets sent back to the browser
}).listen(3000);`

3. File System (fs) Module — reading and writing files​

  • Browser JS cannot touch your filesystem for security reasons — Node can
    • fs.readFile() reads a file and gives you the contents inside a callback (async!)
    • fs.writeFile() creates or overwrites a file on disk
    • The "utf8" argument tells Node to give you the file as readable text, not raw bytes
fs.readFile("notes.txt", "utf8", (err, data) => {
if (err) throw err;
console.log(data); // prints file contents to terminal
});

fs.writeFile("notes.txt", "new content", (err) => {
if (err) throw err;
console.log("File saved!");
});

4. URL Module — parsing a URL into readable parts​

  • When a request comes into your server, you need to know what page they're asking for
  • The URL module breaks a full URL string into useful pieces you can work with
const url = new URL("https://example.com/about?name=cam");
url.pathname // "/about" → which page they want
url.searchParams.get("name") // "cam" → query string values

5. Events / EventEmitter — creating your own custom events​

  • Node has a built-in EventEmitter class you can use to create, fire, and listen for your own events
  • .on("eventName", callback) → listen for an event (same idea as addEventListener in the browser)
  • .emit("eventName") → manually trigger/fire that event

In real apps you won't use EventEmitter directly that often, but understanding it helps you understand how Node itself works under the hood

Example:
const EventEmitter = require("events");
const emitter = new EventEmitter();

emitter.on("greet", () => console.log("Hello!")); // set up the listener
emitter.emit("greet"); // fire it → prints "Hello!"

Note: require() is the older Node way to import modules — same idea as import in modern JS. You'll see both in the wild.

II. Express​

Express is a minimal, unopinionated backend framework built on top of Node.js

  • Writing a server with raw Node's http module gets verbose fast
  • Express wraps all of that and gives you a cleaner, simpler way to handle requests
  • "Unopinionated" = it doesn't force you to structure your app any particular way

1. Setting Up​

npm init -y        # creates package.json
npm install express
// app.js
const express = require("express");
const app = express();

app.get("/", (req, res) => res.send("Hello, world!"));

const PORT = process.env.PORT || 3000; // use env variable, fallback to 3000
app.listen(PORT, (error) => {
if (error) throw error;
console.log(`Listening on port ${PORT}!`);
});
  • express() initializes your server and stores it in app
  • app.get() defines a route — more on this below
  • app.listen() opens the door on that port, same idea as Node's .listen(3000)
  • process.env.PORT is how you read environment variables — useful so you don't hardcode the port

2. How a Request Travels Through Express​

When a browser visits http://localhost:3000/:

  1. Browser sends a GET request to the / path
  2. Express receives it and stores it in a request object (req)
  3. Express passes it through a chain of middleware functions
  4. The first route that matches the HTTP verb + path handles it
  5. That route sends back a response (res) and the cycle ends

Visiting any URL in a browser is always just sending a GET request to that path. https://theodinproject.com/paths = GET request to /paths at theodinproject.com

3. Routes — matching requests to handlers​

  • A route matches an incoming request by HTTP verb (GET, POST, PUT, DELETE) + path
  • Order matters — Express matches the first route that fits, top to bottom
app.get("/messages", (req, res) => res.send("GET messages"));
app.post("/messages", (req, res) => res.send("POST message"));

a. Route Parameters​

  • Dynamic segments in the path, prefixed with :
  • Express populates req.params automatically
// GET /odin/messages → req.params = { username: "odin" }
app.get("/:username/messages", (req, res) => {
console.log(req.params.username); // "odin"
});

b. Query Parameters​

  • Optional key-value pairs after ? in the URL — not part of the path itself
  • Express populates req.query automatically
// GET /messages?sort=date&direction=asc
// req.query = { sort: "date", direction: "asc" }

Real example you've seen: youtube.com/watch?v=abc123&t=60 — /watch is the path, v and t are query params

c. Routers — organizing routes into files​

  • In a real app with many routes, you split them into separate files by resource
  • Each file exports a Router, mounted in app.js under a base path
// routes/authorRouter.js
const { Router } = require("express");
const authorRouter = Router();

authorRouter.get("/", (req, res) => res.send("All authors"));
authorRouter.get("/:authorId", (req, res) => res.send(`Author: ${req.params.authorId}`));

module.exports = authorRouter;
// app.js
app.use("/authors", authorRouter); // all /authors/* requests go here
app.use("/books", bookRouter);

In your codebase: backend/src/app/router.ts mounts all 32 collection routers — same pattern exactly

4. Controllers & Middleware​

a. Middleware​

  • Any function that sits between the request coming in and the response going out
  • Signature: (req, res, next) — call next() to pass to the next function in the chain
  • If you don't call next() and don't send a response, the request just hangs
function logger(req, res, next) {
console.log(`${req.method} ${req.path}`);
next(); // pass control forward
}
app.use(logger); // runs on every request

Common middleware uses:

  • Auth checks (is this user logged in?)
  • Request validation (is the body shaped correctly?)
  • Logging, CORS, JSON parsing

b. Controllers​

  • Controllers are just functions that handle a specific route — they're also middleware, but their job is to be the final handler that sends the response
  • Named by convention: getAuthorById, createStory, deleteEpisode
// controllers/authorController.js
async function getAuthorById(req, res) {
const { authorId } = req.params;
const author = await db.getAuthorById(Number(authorId));

if (!author) {
res.status(404).send("Author not found");
return; // must return — sending a response doesn't stop the function
}

res.send(`Author: ${author.name}`);
}
module.exports = { getAuthorById };
// routes/authorRouter.js — wire the controller to the route
const { getAuthorById } = require("../controllers/authorController");
authorRouter.get("/:authorId", getAuthorById);

In your codebase: every collection has a {name}.controller.ts — these are exactly this pattern

c. Response Methods​

MethodUse
res.json(data)Send JSON — use this for APIs
res.send(data)General purpose, auto-detects type
res.status(404).send(...)Set status code + send — must chain
res.redirect("/path")Redirect client to another URL

d. Error Handling​

  • Wrap async controllers in try/catch, or just throw — Express v5 auto-catches
  • A special error middleware with 4 params (err, req, res, next) catches all bubbled errors
  • Place it at the very end of app.js
// global error handler — must have 4 params
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).send(err.message);
});
  • You can create custom error classes to carry a status code:
class NotFoundError extends Error {
constructor(message) {
super(message);
this.statusCode = 404;
}
}
// then in a controller:
throw new NotFoundError("Author not found"); // bubbles up to error handler

e. The next function​

CallWhat happens
next()Pass to next middleware
next(error)Skip to error handler middleware

f. Folder Structure​

express-app/
├─ routes/ → routers (one per resource)
├─ controllers/ → handler functions
├─ errors/ → custom error classes
├─ app.js → server setup, mounts routers, error handler at bottom